18.2 初始化
调度器初始化函数schedinit在前面已多次提及,除去内存分配、垃圾回收等操作外,针对自身的初始化无非是MaxMcount、GOMAXPROCS。
proc1.go
func schedinit() { // 设置最大M数量 sched.maxmcount=10000
// 初始化栈空间复用管理链表 stackinit()
// 初始化当前M mcommoninit(g.m)
// 默认值总算从1调整为CPU Core数量了 procs:=int(ncpu) if n:=atoi(gogetenv(“GOMAXPROCS”));n>0{ if n> _MaxGomaxprocs{ n= _MaxGomaxprocs } procs=n }
// 调整P数量 // 注意:此刻所有P都是新建的,所以不可能返回有本地任务的P if procresize(int32(procs)) !=nil{ throw(“unknown runnable goroutine during bootstrap”) } }
GOMAXPROCS默认值总算从1改为CPU Cores了。
因为P的数量有最大限制,所以用一个足够大的数组存储才是最正确的做法。虽然浪费点空间,但省去很多内存增减的麻烦。
runtime2.go
var allp[_MaxGomaxprocs+1]*p
type schedt struct{ pidle puintptr// 空闲P链表 npidle uint32 // 空闲P数量 }
调整P的数量并不意味着全部分配新对象,仅仅做去余补缺即可。
proc1.go
func procresize(nprocs int32) *p { old := gomaxprocs
// 新增 for i := int32(0); i < nprocs; i++ { pp := allp[i]
// 申请新 P 对象
if pp == nil {
pp = new(p)
pp.id = i
pp.status = _Pgcstop
// 保存到 allp
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
// 为 P 分配 cache 对象
if pp.mcache == nil {
if old == 0 && i == 0 {
// bootstrap
pp.mcache = getg().m.mcache
} else {
// 创建 cache
pp.mcache = allocmcache()
}
}
}
// 释放多余的 P for i := nprocs; i < old; i++ { p := allp[i]
// 将本地任务转移到全局队列
for p.runqhead != p.runqtail {
p.runqtail--
gp := p.runq[p.runqtail%uint32(len(p.runq))]
globrunqputhead(gp)
}
if p.runnext != 0 {
globrunqputhead(p.runnext.ptr())
p.runnext = 0
}
// 释放当前 P 绑定的 cache
freemcache(p.mcache)
p.mcache = nil
// 将当前 P 的 G 复用链转移到全局
gfpurge(p)
// 似乎就丢在那里不管了,反正也没剩下啥
p.status = _Pdead
// can't free P itself because it can be referenced by an M in syscall
}
g := getg()
// 如果当前正在用的 P 属于被释放的那拨,那就换成 allp[0] // 调度器初始化阶段,根本没有 P,那就绑定 allp[0] if g.m.p != 0 && g.m.p.ptr().id < nprocs { // 继续使用当前 P g.m.p.ptr().status = _Prunning } else { // 释放当前 P,因为它已经失效 if g.m.p != 0 { g.m.p.ptr().m = 0 } g.m.p = 0 g.m.mcache = nil
// 换成 allp[0]
p := allp[0]
p.m = 0
p.status = _Pidle
acquirep(p)
}
// 将没有本地任务的 P 放到空闲链表 var runnablePs *p for i := nprocs - 1; i >= 0; i— { p := allp[i]
// 确保不是当前正在用的 P
if _g_.m.p.ptr() == p {
continue
}
p.status = _Pidle
if runqempty(p) {
// 放入空闲链表
pidleput(p)
} else {
// 有本地任务,构建链表
p.m.set(mget())
p.link.set(runnablePs)
runnablePs = p
}
}
// 返回有本地任务的 P(链表) return runnablePs }
// 将 P 放入空闲链表 func pidleput(p *p) { p.link = sched.pidle sched.pidle.set(p) xadd(&sched.npidle, 1) }
默认只有schedinit和startTheWorld会调用procresize函数。在调度器初始化阶段,所有P对象都是新建的。除分配给当前主线程的外,其他都被放入空闲链表。而startTheWorld会激活全部有本地任务的P对象(详见后文)。
在完成调度器初始化后,引导过程才创建并运行main goroutine。
asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT,$0 // save m→g0 = g0 MOVQ CX, m_g0(AX) // save m0 to g0→m MOVQ AX, g_m(CX)
CALL runtime·schedinit(SB)
// 创建 main goroutine,并将其放入当前 P 本地队列
MOVQ 0
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// 让当前 M0 进入调度,执行 main goroutine CALL runtime·mstart(SB)
// M0 永远不会执行这条崩溃测试指令 MOVL $0xf1, 0xf1 // crash RET
虽然可在运行期用runtime.GOMAXPROCS函数修改P的数量,但须付出极大代价。
debug.go
func GOMAXPROCS(n int) int { if n > _MaxGomaxprocs { n = _MaxGomaxprocs }
// 返回当前值(这个才是最常用的做法) ret := int(gomaxprocs) if n ⇐ 0 || n == ret { return ret }
// STW !!! stopTheWorld(“GOMAXPROCS”)
newprocs = int32(n)
// 调用 procresize,并激活有任务的 P startTheWorld()
return ret }